/*
 * Copyright 2017-2021 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micronaut.inject.beans;

import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Introspected;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import io.micronaut.core.annotation.UsedByGeneratedCode;
import io.micronaut.core.beans.BeanConstructor;
import io.micronaut.core.beans.BeanIntrospection;
import io.micronaut.core.beans.BeanMethod;
import io.micronaut.core.beans.BeanProperty;
import io.micronaut.core.beans.BeanReadProperty;
import io.micronaut.core.beans.BeanWriteProperty;
import io.micronaut.core.beans.UnsafeBeanInstantiationIntrospection;
import io.micronaut.core.beans.UnsafeBeanProperty;
import io.micronaut.core.beans.UnsafeBeanReadProperty;
import io.micronaut.core.beans.UnsafeBeanWriteProperty;
import io.micronaut.core.beans.exceptions.IntrospectionException;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.core.reflect.exception.InstantiationException;
import io.micronaut.core.type.Argument;
import io.micronaut.core.type.ReturnType;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringIntMap;
import io.micronaut.inject.ExecutableMethod;
import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
 * Abstract implementation of the {@link BeanIntrospection} interface. This class is subclasses at compilation time by generated byte code and should not be used directly.
 * <p>
 * Implementation is using method dispatch to access the bean instance.
 *
 * @param <B> The bean type
 * @author Denis Stepanov
 * @since 3.1
 */
public abstract class AbstractInitializableBeanIntrospection<B> implements UnsafeBeanInstantiationIntrospection<B> {

    private final Class<B> beanType;
    private final AnnotationMetadata annotationMetadata;
    private final AnnotationMetadata constructorAnnotationMetadata;
    private final Argument<?>[] constructorArguments;
    private final BeanProperty<B, Object>[] beanProperties;
    private final List<BeanProperty<B, Object>> beanPropertiesList;
    private final List<BeanReadProperty<B, Object>> beanReadPropertiesList;
    private final List<BeanWriteProperty<B, Object>> beanWritePropertiesList;
    private final List<BeanMethod<B, Object>> beanMethodsList;
    private final StringIntMap beanPropertyIndex;

    private BeanConstructor<B> beanConstructor;

    private IntrospectionBuilderData builderData;

    protected AbstractInitializableBeanIntrospection(Class<B> beanType,
                                                  AnnotationMetadata annotationMetadata,
                                                  AnnotationMetadata constructorAnnotationMetadata,
                                                  Argument<?>[] constructorArguments,
                                                  BeanPropertyRef<Object>[] propertiesRefs,
                                                  BeanMethodRef<Object>[] methodsRefs) {
        this.beanType = beanType;
        this.annotationMetadata = annotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : EvaluatedAnnotationMetadata.wrapIfNecessary(annotationMetadata);
        this.constructorAnnotationMetadata = constructorAnnotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : EvaluatedAnnotationMetadata.wrapIfNecessary(constructorAnnotationMetadata);
        this.constructorArguments = constructorArguments == null ? Argument.ZERO_ARGUMENTS : constructorArguments;

        if (propertiesRefs != null) {
            List<BeanProperty<B, Object>> beanProperties = new ArrayList<>(propertiesRefs.length);
            List<BeanReadProperty<B, Object>> beanReadProperties = new ArrayList<>(propertiesRefs.length);
            List<BeanWriteProperty<B, Object>> beanWriteProperties = new ArrayList<>(propertiesRefs.length);
            for (BeanPropertyRef<Object> beanPropertyRef : propertiesRefs) {
                if (beanPropertyRef.readyOnly) {
                    if (beanPropertyRef.readArgument != null) {
                        beanReadProperties.add(new BeanReadPropertyImpl<>(beanPropertyRef));
                    } else {
                        beanReadProperties.add(new BeanPropertyImpl<>(beanPropertyRef));
                    }
                } else if (beanPropertyRef.writeOnly) {
                    if (beanPropertyRef.writeArgument != null) {
                        beanWriteProperties.add(new BeanWritePropertyImpl<>(beanPropertyRef));
                    } else {
                        beanWriteProperties.add(new BeanPropertyImpl<>(beanPropertyRef));
                    }
                } else {
                    if (beanPropertyRef.readArgument != null) {
                        beanReadProperties.add(new BeanReadPropertyImpl<>(beanPropertyRef));
                    } else {
                        beanReadProperties.add(new BeanPropertyImpl<>(beanPropertyRef));
                    }
                    if (beanPropertyRef.writeArgument != null) {
                        beanWriteProperties.add(new BeanWritePropertyImpl<>(beanPropertyRef));
                    } else {
                        beanWriteProperties.add(new BeanPropertyImpl<>(beanPropertyRef));
                    }
                }
                beanProperties.add(new BeanPropertyImpl<>(beanPropertyRef));
            }
            this.beanProperties = beanProperties.toArray(BeanProperty[]::new);
            this.beanPropertiesList = Collections.unmodifiableList(beanProperties);
            this.beanReadPropertiesList = Collections.unmodifiableList(beanReadProperties);
            this.beanWritePropertiesList = Collections.unmodifiableList(beanWriteProperties);
        } else {
            this.beanProperties = new BeanProperty[0];
            this.beanPropertiesList = Collections.emptyList();
            this.beanReadPropertiesList = Collections.emptyList();
            this.beanWritePropertiesList = Collections.emptyList();
        }
        this.beanPropertyIndex = new StringIntMap(beanProperties.length);
        for (int i = 0; i < beanProperties.length; i++) {
            beanPropertyIndex.put(beanProperties[i].getName(), i);
        }
        if (methodsRefs != null) {
            List<BeanMethod<B, Object>> beanMethods = new ArrayList<>(methodsRefs.length);
            for (BeanMethodRef<Object> beanMethodRef : methodsRefs) {
                beanMethods.add(new BeanMethodImpl<>(beanMethodRef));
            }
            this.beanMethodsList = Collections.unmodifiableList(beanMethods);
        } else {
            this.beanMethodsList = Collections.emptyList();
        }
    }

    @Override
    public Builder<B> builder() {
        if (isBuildable()) {
            return new IntrospectionBuilder<>(this);
        } else {
            throw new IntrospectionException("No accessible constructor or builder exists for type: " + getBeanType().getName());
        }
    }

    /**
     * Whether an accessible constructor exists.
     * @return True if a default constructor exists
     * @since 4.7.11
     */
    protected boolean hasConstructor() {
        return false;
    }

    /**
     * Reflection free bean instantiation implementation for the given arguments.
     *
     * @param arguments The arguments
     * @return The bean
     */
    @NonNull
    @Internal
    @UsedByGeneratedCode
    protected B instantiateInternal(@Nullable Object[] arguments) {
        if (hasBuilder() && arguments != null) {
            Builder<B> b = builder();
            @NonNull Argument<?>[] args = b.getBuilderArguments();
            for (int i = 0; i < args.length; i++) {
                Argument<Object> arg = (Argument<Object>) args[i];
                Object val = arguments[i];
                b.with(i, arg, val);
            }
            @NonNull Argument<?>[] buildMethodArguments = b.getBuildMethodArguments();
            if (buildMethodArguments.length == 0) {
                return b.build();
            } else {
                @Nullable Object[] buildParams = Arrays.copyOfRange(arguments, args.length, arguments.length);
                return b.build(buildParams);
            }

        } else {
            throw new InstantiationException("Type [" + getBeanType() + "] defines no accessible constructor");
        }
    }

    /**
     * Obtain a property by its index.
     *
     * @param index The index of the property
     * @return A bean property
     */
    @Internal
    @UsedByGeneratedCode
    protected BeanProperty<B, Object> getPropertyByIndex(int index) {
        return beanProperties[index];
    }

    @Override
    public int propertyIndexOf(String name) {
        return beanPropertyIndex.get(name, -1);
    }

    /**
     * Find {@link Method} representation at the method by index. Used by {@link ExecutableMethod#getTargetMethod()}.
     *
     * @param index The index
     * @return The method
     */
    @UsedByGeneratedCode
    @Internal
    protected abstract Method getTargetMethodByIndex(int index);

    /**
     * Find {@link Method} representation at the method by index. Used by {@link ExecutableMethod#getTargetMethod()}.
     *
     * @param index The index
     * @return The method
     * @since 3.8.5
     */
    @UsedByGeneratedCode
    // this logic must allow reflection
    @SuppressWarnings("java:S3011")
    protected final Method getAccessibleTargetMethodByIndex(int index) {
        Method method = getTargetMethodByIndex(index);
        if (ClassUtils.REFLECTION_LOGGER.isDebugEnabled()) {
            ClassUtils.REFLECTION_LOGGER.debug("Reflectively accessing method {} of type {}", method, method.getDeclaringClass());
        }
        method.setAccessible(true);
        return method;
    }

    /**
     * Triggers the invocation of the method at index.
     *
     * @param index  The method index
     * @param target The target
     * @param args   The arguments
     * @param <V>    The result type
     * @return The result
     */
    @Nullable
    @UsedByGeneratedCode
    protected <V> V dispatch(int index, @NonNull B target, @Nullable Object[] args) {
        throw unknownDispatchAtIndexException(index);
    }

    /**
     * Triggers the invocation of the method at index for a single argument call.
     * Allowing to not wrap a single argument in an object array.
     *
     * @param index  The method index
     * @param target The target
     * @param arg    The argument
     * @param <V> The result type
     * @return The result
     */
    @Nullable
    @UsedByGeneratedCode
    protected <V> V dispatchOne(int index, @NonNull Object target, @Nullable Object arg) {
        throw unknownDispatchAtIndexException(index);
    }

    /**
     * Creates a new exception when the dispatch at index is not found.
     *
     * @param index The method index
     * @return The exception
     */
    @UsedByGeneratedCode
    protected final RuntimeException unknownDispatchAtIndexException(int index) {
        return new IllegalStateException("Unknown dispatch at index: " + index);
    }

    /**
     * Get all the bean properties annotated for the given type.
     * Nullable result method version of {@link #getIndexedProperty(Class, String)}.
     *
     * @param annotationType  The annotation type
     * @param annotationValue The annotation value
     * @return An immutable collection of properties.
     * @see io.micronaut.core.annotation.Introspected#indexed()
     */
    @Nullable
    @UsedByGeneratedCode
    public BeanProperty<B, Object> findIndexedProperty(@NonNull Class<? extends Annotation> annotationType, @NonNull String annotationValue) {
        return null;
    }

    @NonNull
    @Override
    @UsedByGeneratedCode
    public Collection<BeanProperty<B, Object>> getIndexedProperties(@NonNull Class<? extends Annotation> annotationType) {
        return Collections.emptyList();
    }

    /**
     * Returns subset of bean properties defined by an array of indexes.
     *
     * @param indexes The indexes
     * @return a collection of bean properties
     */
    @UsedByGeneratedCode
    protected Collection<BeanProperty<B, Object>> getBeanPropertiesIndexedSubset(int[] indexes) {
        if (indexes.length == 0) {
            return Collections.emptyList();
        }
        return new IndexedCollections<>(indexes, beanProperties);
    }

    @Override
    public B instantiate() throws InstantiationException {
        throw new InstantiationException("No default constructor exists");
    }

    @NonNull
    @Override
    public B instantiate(boolean strictNullable, Object... arguments) throws InstantiationException {
        ArgumentUtils.requireNonNull("arguments", arguments);

        if (arguments.length == 0) {
            return instantiate();
        }

        final Argument<?>[] constructorArguments = getConstructorArguments();
        if (constructorArguments.length != arguments.length) {
            throw new InstantiationException("Argument count [" + arguments.length + "] doesn't match required argument count: " + constructorArguments.length);
        }

        for (int i = 0; i < constructorArguments.length; i++) {
            Argument<?> constructorArgument = constructorArguments[i];
            final Object specified = arguments[i];
            if (specified == null) {
                if (constructorArgument.isDeclaredNullable() || !strictNullable) {
                    continue;
                } else {
                    throw new InstantiationException("Null argument specified for [" + constructorArgument.getName() + "]. If this argument is allowed to be null annotate it with @Nullable");
                }
            }
            if (!ReflectionUtils.getWrapperType(constructorArgument.getType()).isInstance(specified)) {
                throw new InstantiationException("Invalid argument [" + specified + "] specified for argument: " + constructorArgument);
            }
        }

        return instantiateInternal(arguments);
    }

    @Override
    public B instantiateUnsafe(@NonNull Object... arguments) {
        return instantiateInternal(arguments);
    }

    @Override
    public BeanConstructor<B> getConstructor() {
        if (beanConstructor == null) {
            beanConstructor = new BeanConstructor<>() {
                @Override
                public Class<B> getDeclaringBeanType() {
                    return beanType;
                }

                @Override
                public Argument<?>[] getArguments() {
                    return constructorArguments;
                }

                @Override
                public B instantiate(Object... parameterValues) {
                    return AbstractInitializableBeanIntrospection.this.instantiate(parameterValues);
                }

                @Override
                public AnnotationMetadata getAnnotationMetadata() {
                    return constructorAnnotationMetadata;
                }
            };
        }
        return beanConstructor;
    }

    @Override
    public Argument<?>[] getConstructorArguments() {
        if (hasConstructor()) {
            return constructorArguments;
        } else {
            return hasBuilder() ? getBuilderData().constructorArguments : constructorArguments;
        }
    }

    @NonNull
    @Override
    public Optional<BeanProperty<B, Object>> getIndexedProperty(@NonNull Class<? extends Annotation> annotationType, @NonNull String annotationValue) {
        return Optional.ofNullable(findIndexedProperty(annotationType, annotationValue));
    }

    @NonNull
    @Override
    public Optional<BeanProperty<B, Object>> getProperty(@NonNull String name) {
        ArgumentUtils.requireNonNull("name", name);
        int index = propertyIndexOf(name);
        return index == -1 ? Optional.empty() : Optional.of(beanProperties[index]);
    }

    @Override
    public AnnotationMetadata getAnnotationMetadata() {
        return annotationMetadata;
    }

    @NonNull
    @Override
    public Collection<BeanProperty<B, Object>> getBeanProperties() {
        return beanPropertiesList;
    }

    @Override
    public List<BeanReadProperty<B, Object>> getBeanReadProperties() {
        return beanReadPropertiesList;
    }

    @Override
    public List<BeanWriteProperty<B, Object>> getBeanWriteProperties() {
        return beanWritePropertiesList;
    }

    @NonNull
    @Override
    public Class<B> getBeanType() {
        return beanType;
    }

    @NonNull
    @Override
    public Collection<BeanMethod<B, Object>> getBeanMethods() {
        return beanMethodsList;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        AbstractInitializableBeanIntrospection<?> that = (AbstractInitializableBeanIntrospection<?>) o;
        return Objects.equals(beanType, that.beanType);
    }

    @Override
    public int hashCode() {
        return beanType.hashCode();
    }

    @Override
    public String toString() {
        return "BeanIntrospection{type=" + beanType + '}';
    }

    @SuppressWarnings("unchecked")
    @NonNull
    private IntrospectionBuilderData getBuilderData() {
        if (this.builderData == null) {

            AnnotationValue<Introspected.IntrospectionBuilder> builderAnn = getAnnotationMetadata().findAnnotation(Introspected.class)
                .flatMap(a -> a.getAnnotation("builder", Introspected.IntrospectionBuilder.class)).orElse(null);
            Class<?> builderClass = getAnnotationMetadata().classValue(Introspected.class, "builderClass").orElse(null);
            if (builderAnn != null || builderClass != null) {
                if (builderClass == null) {
                    throw new IntrospectionException("Introspection defines invalid builder member for type: " + getBeanType());
                } else {
                    BeanIntrospection<Object> builderIntrospection = (BeanIntrospection<Object>) BeanIntrospection.getIntrospection(builderClass);
                    Collection<BeanMethod<Object, Object>> beanMethods = builderIntrospection.getBeanMethods();

                    // find the creator method
                    BeanMethod<Object, Object> constructorMethod = beanMethods.stream()
                        .filter(m -> m.getReturnType().getType().equals(getBeanType()))
                        .findFirst().orElse(null);
                    if (constructorMethod == null) {
                        throw new IntrospectionException("No build method found in builder: " + builderClass.getName());
                    } else {
                        BeanMethod<Object, Object>[] builderMethods = beanMethods.stream()
                            .filter(m ->
                                m.getReturnType().getType().isAssignableFrom(builderIntrospection.getBeanType())
                            )
                            .toArray(BeanMethod[]::new);

                        List<Argument<?>> arguments = new ArrayList<>(builderMethods.length);
                        Set<String> properties = CollectionUtils.newHashSet(builderMethods.length);
                        Set<BeanMethod<?, ?>> excludedMethods = new HashSet<>();
                        for (BeanMethod<Object, Object> builderMethod : builderMethods) {
                            Argument<?> argument;
                            @NonNull Argument<?>[] methodArgs = builderMethod.getArguments();
                            if (ArrayUtils.isNotEmpty(methodArgs)) {
                                argument = toWrapperIfNecessary(methodArgs[0]);
                            } else {
                                argument = Argument.of(Boolean.class, builderMethod.getName());
                            }
                            if (!properties.add(argument.getName())) {
                                excludedMethods.add(builderMethod);
                            } else {
                                arguments.add(argument);
                            }
                        }
                        if (!excludedMethods.isEmpty()) {
                            builderMethods = Arrays.stream(builderMethods)
                                .filter(bm -> !excludedMethods.contains(bm))
                                .toArray(BeanMethod[]::new);
                        }
                        this.builderData = new IntrospectionBuilderData(
                            builderIntrospection,
                            constructorMethod,
                            builderMethods,
                            arguments.toArray(Argument.ZERO_ARGUMENTS)
                        );
                    }
                }
            } else {
                int constructorLength = constructorArguments.length;
                @NonNull UnsafeBeanProperty<B, Object>[] writeableProperties = resolveWriteableProperties(beanPropertiesList);

                this.builderData = new IntrospectionBuilderData(
                    constructorArguments,
                    constructorLength,
                    (UnsafeBeanProperty<Object, Object>[]) writeableProperties
                );
            }
        }
        return builderData;
    }

    @NonNull
    private static Argument<?> toWrapperIfNecessary(Argument<?> argument) {
        if (argument.isPrimitive()) {
            // alway use wrapper type
            Class<?> wrapperType = argument.getWrapperType();
            return Argument.of(wrapperType, argument.getName(), argument.getAnnotationMetadata());
        }
        return argument;
    }

    @SuppressWarnings("unchecked")
    @NonNull
    private <P> UnsafeBeanProperty<P, Object>[] resolveWriteableProperties(Collection<BeanProperty<P, Object>> beanProperties) {
        return beanProperties.stream()
            .filter(bp -> !bp.isReadOnly() && Arrays.stream(constructorArguments).noneMatch(a -> bp.getName().equals(a.getName())))
            .map(bp -> ((UnsafeBeanProperty<P, Object>) bp))
            .toArray(UnsafeBeanProperty[]::new);
    }

    @SuppressWarnings("java:S6218")
    private record IntrospectionBuilderData(
        Argument<?>[] arguments,
        Argument<?>[] constructorArguments,
        int constructorLength,
        @Nullable
        UnsafeBeanProperty<Object, Object>[] writeableProperties,
        @Nullable
        BeanIntrospection<Object> builder,
        @Nullable
        BeanMethod<Object, Object> creator,
        @Nullable
        BeanMethod<Object, Object>[] buildMethods,
        StringIntMap argumentIndex,
        Object[] defaultValues,
        boolean[] required) {

        public IntrospectionBuilderData(
            Argument<?>[] constructorArguments,
            int constructorLength,
            UnsafeBeanProperty<Object, Object>[] writeableProperties) {
            this(
                toArguments(constructorArguments, constructorLength, writeableProperties),
                constructorLength,
                writeableProperties,
                null,
                null,
                null,
                new StringIntMap(constructorLength + writeableProperties.length),
                new Object[constructorLength + writeableProperties.length],
                toRequires(constructorLength, constructorArguments, writeableProperties));
            init(arguments);
        }

        @NonNull
        private static boolean[] toRequires(int constructorLength, Argument<?>[] constructorArguments, UnsafeBeanProperty<Object, Object>[] writeableProperties) {
            boolean[] requires = new boolean[constructorLength + writeableProperties.length];
            for (int i = 0; i < constructorLength; i++) {
                Argument<?> argument = constructorArguments[i];
                requires[i] = argument.getType().isPrimitive() || argument.isDeclaredNonNull();
            }
            for (int i = constructorLength; i < requires.length; i++) {
                UnsafeBeanProperty<Object, Object> writeableProperty = writeableProperties[i - constructorLength];
                Argument<Object> argument = writeableProperty.asArgument();
                requires[i] = argument.getType().isPrimitive() || argument.isDeclaredNonNull();

            }
            return requires;
        }

        public IntrospectionBuilderData(
            BeanIntrospection<Object> builder,
            BeanMethod<Object, Object> creator,
            BeanMethod<Object, Object>[] buildMethods,
            Argument<?>[] arguments) {
            this(arguments, 0, null, builder, creator, buildMethods, new StringIntMap(arguments.length), new Object[arguments.length], new boolean[arguments.length]);
            init(arguments);
        }

        public IntrospectionBuilderData(
            Argument<?>[] arguments,
            int constructorLength,
            @Nullable
            UnsafeBeanProperty<Object, Object>[] writeableProperties,
            @Nullable
            BeanIntrospection<Object> builder,
            @Nullable
            BeanMethod<Object, Object> creator,
            @Nullable
            BeanMethod<Object, Object>[] buildMethods,
            StringIntMap argumentIndex,
            Object[] defaultValues,
            boolean[] required) {
            this(arguments, creator == null ? arguments : ArrayUtils.concat(arguments, creator.getArguments()), constructorLength, writeableProperties, builder, creator, buildMethods, argumentIndex, defaultValues, required);
        }

        static Argument<?>[] toArguments(Argument<?>[] constructorArguments, int constructorLength, UnsafeBeanProperty<Object, Object>[] writeableProperties) {
            Argument<?>[] propertyArguments = toArguments(writeableProperties);
            Argument<?>[] arguments;
            if (constructorLength == 0) {
                arguments = propertyArguments;
            } else {
                Argument<?>[] newConstructorArguments = new Argument[constructorLength];
                for (int i = 0; i < constructorLength; i++) {
                    Argument<?> constructorArgument = constructorArguments[i];
                    Argument<?> argument = toWrapperIfNecessary(constructorArgument);
                    newConstructorArguments[i] = argument;
                }
                arguments = ArrayUtils.concat(newConstructorArguments, propertyArguments);
            }
            return arguments;
        }

        @NonNull
        private static Argument<?>[] toArguments(BeanProperty<?, ?>[] writeableProperties) {
            return Arrays.stream(writeableProperties)
                .map(bp -> {
                    Argument<?> argument = bp.asArgument();
                    return toWrapperIfNecessary(argument);
                })
                .toArray(Argument[]::new);
        }

        private void init(Argument<?>[] arguments) {
            for (int i = 0; i < arguments.length; i++) {
                Argument<?> argument = arguments[i];
                argumentIndex.put(argument.getName(), i);
                defaultValues[i] = argument.getAnnotationMetadata().getValue(Bindable.class, "defaultValue", argument).orElse(null);
            }
        }
    }

    private static final class IntrospectionBuilder<B> implements Builder<B> {
        private static final Object[] NULL_ARG = { null };
        private final Object[] params;
        private final IntrospectionBuilderData builderData;
        private final AbstractInitializableBeanIntrospection<B> introspection;

        IntrospectionBuilder(AbstractInitializableBeanIntrospection<B> outer) {
            IntrospectionBuilderData data = outer.getBuilderData();
            this.introspection = outer;
            this.builderData = data;
            this.params = new Object[data.arguments.length];
        }

        @Override
        public @NonNull Argument<?>[] getBuilderArguments() {
            return builderData.arguments;
        }

        @Override
        public @NonNull Argument<?>[] getBuildMethodArguments() {
            return builderData.creator.getArguments();
        }

        @Override
        public int indexOf(String name) {
            return builderData.argumentIndex.get(name, -1);
        }

        @Override
        public @NonNull Builder<B> with(String name, Object value) {
            int i = indexOf(name);
            if (i != -1) {
                @SuppressWarnings("unchecked")
                Argument<Object> argument = (Argument<Object>) builderData.arguments[i];
                return with(i, argument, value);
            }
            return this;
        }

        @Override
        public @NonNull <A> Builder<B> with(int index, Argument<A> argument, A value) {
            if (value != null) {
                if (!argument.isInstance(value)) {
                    throw new IllegalArgumentException("Invalid value [" + value + "] specified for argument [" + argument + "]");
                }
                params[index] = value;
            }
            return this;
        }

        @Override
        public @NonNull <A> Builder<B> convert(int index, ArgumentConversionContext<A> conversionContext, Object value, ConversionService conversionService) {
            Argument<A> argument = conversionContext.getArgument();
            if (value != null) {
                if (!argument.isInstance(value)) {
                    value = conversionService.convertRequired(value, conversionContext);
                }
                params[index] = value;
            }
            return this;
        }

        @Override
        public Builder<B> with(B existing) {
            if (existing != null) {
                Collection<BeanProperty<B, Object>> properties = introspection.getBeanProperties();
                for (BeanProperty<B, Object> property : properties) {
                    if (!property.isWriteOnly() && property.hasSetterOrConstructorArgument()) {
                        int i = indexOf(property.getName());
                        if (i > -1) {
                            with(i, (Argument<Object>) builderData.arguments[i], property.get(existing));
                        }
                    }
                }
            }
            return this;
        }

        @Override
        public B build() {
            return build(ArrayUtils.EMPTY_OBJECT_ARRAY);
        }

        @Override
        public B build(Object... builderParams) {
            for (int i = 0; i < builderData.required.length; i++) {
                if (builderData.required[i] && params[i] == null) {
                    throw new IllegalArgumentException("Non-null argument [" + builderData.arguments[i] + "] specified as a null");
                }
            }
            BeanIntrospection<Object> builderIntrospection = builderData.builder;
            if (builderIntrospection != null) {
                Object b = builderIntrospection.instantiate();
                BeanMethod<Object, Object> creator = builderData.creator;
                for (int i = 0; i < params.length; i++) {
                    Object param = params[i];
                    BeanMethod<Object, Object> m = builderData.buildMethods[i];
                    if (param instanceof Boolean bool) {
                        if (m.getArguments().length == 0) {
                            if (Boolean.TRUE.equals(bool)) {
                                Object r = m.invoke(b);
                                if (r != null) {
                                    b = r;
                                }
                            }
                        } else {
                            Object r = m.invoke(b, bool);
                            if (r != null) {
                                b = r;
                            }
                        }
                    } else {

                        Object r = null;
                        if (param != null) {
                            r = m.invoke(b, param);
                        } else {
                            if (builderData.arguments[i].isDeclaredNullable()) {
                                r = m.invoke(b, NULL_ARG);
                            }
                        }
                        if (r != null) {
                            b = r;
                        }
                    }
                }
                if (creator.getArguments().length != builderParams.length) {
                    throw new InstantiationException("Build method " + creator + " expects [" + creator.getArguments().length + "] arguments, but " + builderParams.length + " were provided");
                } else {
                    return (B) creator.invoke(b, builderParams);
                }
            } else {
                int constructorLength = builderData.constructorLength;
                if (constructorLength == params.length) {
                    return introspection.instantiateInternal(params);
                } else {
                    Object[] constructorParams = new Object[constructorLength];
                    System.arraycopy(params, 0, constructorParams, 0, constructorLength);
                    B bean = introspection.instantiateInternal(constructorParams);
                    UnsafeBeanProperty<Object, Object>[] writeableProperties = builderData.writeableProperties;
                    if (writeableProperties != null) {
                        for (int i = constructorLength; i < builderData.arguments.length; i++) {
                            UnsafeBeanProperty<Object, Object> property = writeableProperties[i - constructorLength];
                            Object v = params[i];
                            property.setUnsafe(bean, v);
                        }
                    }
                    return bean;
                }
            }
        }
    }

    /**
     * A subset collection that is defined by an array of indexes into other collection.
     *
     * @param <T> The item type
     */
    private static final class IndexedCollections<T> extends AbstractCollection<T> {

        private final int[] indexed;
        private final T[] array;

        private IndexedCollections(int[] indexed, T[] array) {
            this.indexed = indexed;
            this.array = array;
        }

        @Override
        public Iterator<T> iterator() {
            return new Iterator<>() {

                int i = -1;

                @Override
                public boolean hasNext() {
                    return i + 1 < indexed.length;
                }

                @Override
                public T next() {
                    if (!hasNext()) {
                        throw new NoSuchElementException();
                    }
                    int index = indexed[++i];
                    return array[index];
                }
            };
        }

        @Override
        public int size() {
            return indexed.length;
        }

    }

    /**
     * Implementation of {@link UnsafeBeanProperty} that is using {@link BeanPropertyRef} and method dispatch.
     *
     * @param <P> The property type
     */
    private final class BeanPropertyImpl<P> implements UnsafeBeanProperty<B, P> {

        private final BeanPropertyRef<P> ref;
        private final Class<?> typeOrWrapperType;
        private final AnnotationMetadata annotationMetadata;

        private BeanPropertyImpl(BeanPropertyRef<P> ref) {
            this.ref = ref;
            this.typeOrWrapperType = ReflectionUtils.getWrapperType(getType());
            this.annotationMetadata = EvaluatedAnnotationMetadata.wrapIfNecessary(ref.argument.getAnnotationMetadata());
        }

        @NonNull
        @Override
        public String getName() {
            return ref.argument.getName();
        }

        @NonNull
        @Override
        public Class<P> getType() {
            return ref.argument.getType();
        }

        @Override
        @NonNull
        public Argument<P> asArgument() {
            return ref.argument;
        }

        @NonNull
        @Override
        public BeanIntrospection<B> getDeclaringBean() {
            return AbstractInitializableBeanIntrospection.this;
        }

        @Override
        public AnnotationMetadata getAnnotationMetadata() {
            return annotationMetadata;
        }

        @Nullable
        @Override
        public P get(@NonNull B bean) {
            ArgumentUtils.requireNonNull("bean", bean);
            if (!beanType.isInstance(bean)) {
                throw new IllegalArgumentException("Invalid bean [" + bean + "] for type: " + beanType);
            }
            if (isWriteOnly()) {
                throw new UnsupportedOperationException("Cannot read from a write-only property: " + getName());
            }
            return dispatchOne(ref.getMethodIndex, bean, null);
        }

        @Override
        public P getUnsafe(B bean) {
            return dispatchOne(ref.getMethodIndex, bean, null);
        }

        @Override
        public void set(@NonNull B bean, @Nullable P value) {
            ArgumentUtils.requireNonNull("bean", bean);

            if (!beanType.isInstance(bean)) {
                throw new IllegalArgumentException("Invalid bean [" + bean + "] for type: " + beanType);
            }
            if (isReadOnly()) {
                throw new UnsupportedOperationException("Cannot write a read-only property: " + getName());
            }
            if (value != null && !typeOrWrapperType.isInstance(value)) {
                throw new IllegalArgumentException("Specified value [" + value + "] is not of the correct type: " + getType());
            }
            dispatchOne(ref.setMethodIndex, bean, value);
        }

        @Override
        public void setUnsafe(B bean, P value) {
            dispatchOne(ref.setMethodIndex, bean, value);
        }

        @Override
        public B withValue(@NonNull B bean, @Nullable P value) {
            ArgumentUtils.requireNonNull("bean", bean);
            if (!beanType.isInstance(bean)) {
                throw new IllegalArgumentException("Invalid bean [" + bean + "] for type: " + beanType);
            }
            return withValueUnsafe(bean, value);
        }

        @Override
        public B withValueUnsafe(B bean, P value) {
            if (value == getUnsafe(bean)) {
                return bean;
            } else if (ref.withMethodIndex == -1) {
                if (!ref.readyOnly && ref.setMethodIndex != -1) {
                    dispatchOne(ref.setMethodIndex, bean, value);
                    return bean;
                }
                return UnsafeBeanProperty.super.withValue(bean, value);
            } else {
                return dispatchOne(ref.withMethodIndex, bean, value);
            }
        }

        @Override
        public boolean isReadOnly() {
            return ref.readyOnly;
        }

        @Override
        public boolean isWriteOnly() {
            return ref.writeOnly;
        }

        @Override
        public boolean hasSetterOrConstructorArgument() {
            return ref.mutable;
        }

        @Override
        public String toString() {
            return "BeanProperty{" +
                    "beanType=" + beanType +
                    ", type=" + ref.argument.getType() +
                    ", name='" + ref.argument.getName() + '\'' +
                    '}';
        }
    }

    /**
     * Implementation of {@link UnsafeBeanWriteProperty} that is using {@link BeanPropertyRef} and method dispatch.
     *
     * @param <P> The property type
     */
    private final class BeanWritePropertyImpl<P> implements UnsafeBeanWriteProperty<B, P> {

        private final Argument<P> argument;
        private final Class<?> typeOrWrapperType;
        private final AnnotationMetadata annotationMetadata;
        private final int setMethodIndex;
        private final int withMethodIndex;

        private BeanWritePropertyImpl(BeanPropertyRef<P> ref) {
            this.argument = ref.writeArgument;
            this.typeOrWrapperType = ReflectionUtils.getWrapperType(getType());
            this.annotationMetadata = EvaluatedAnnotationMetadata.wrapIfNecessary(argument.getAnnotationMetadata());
            this.setMethodIndex = ref.setMethodIndex;
            this.withMethodIndex = ref.withMethodIndex;
        }

        @NonNull
        @Override
        public String getName() {
            return argument.getName();
        }

        @NonNull
        @Override
        public Class<P> getType() {
            return argument.getType();
        }

        @Override
        @NonNull
        public Argument<P> asArgument() {
            return argument;
        }

        @NonNull
        @Override
        public BeanIntrospection<B> getDeclaringBean() {
            return AbstractInitializableBeanIntrospection.this;
        }

        @Override
        public AnnotationMetadata getAnnotationMetadata() {
            return annotationMetadata;
        }

        @Override
        public void set(@NonNull B bean, @Nullable P value) {
            ArgumentUtils.requireNonNull("bean", bean);

            if (!beanType.isInstance(bean)) {
                throw new IllegalArgumentException("Invalid bean [" + bean + "] for type: " + bean);
            }
            if (value != null && !typeOrWrapperType.isInstance(value)) {
                throw new IllegalArgumentException("Specified value [" + value + "] is not of the correct type: " + getType());
            }
            dispatchOne(setMethodIndex, bean, value);
        }

        @Override
        public void setUnsafe(B bean, P value) {
            dispatchOne(setMethodIndex, bean, value);
        }

        @Override
        public B withValue(@NonNull B bean, @Nullable P value) {
            ArgumentUtils.requireNonNull("bean", bean);
            if (!beanType.isInstance(bean)) {
                throw new IllegalArgumentException("Invalid bean [" + bean + "] for type: " + beanType);
            }
            return withValueUnsafe(bean, value);
        }

        @Override
        public B withValueUnsafe(B bean, P value) {
            if (withMethodIndex == -1) {
                dispatchOne(setMethodIndex, bean, value);
                return bean;
            } else {
                return dispatchOne(withMethodIndex, bean, value);
            }
        }

        @Override
        public String toString() {
            return "BeanWriteProperty{" +
                    "beanType=" + beanType +
                    ", type=" + argument.getType() +
                    ", name='" + argument.getName() + '\'' +
                    '}';
        }
    }

    /**
     * Implementation of {@link io.micronaut.core.beans.UnsafeBeanReadProperty} that is using {@link BeanPropertyRef} and method dispatch.
     *
     * @param <P> The property type
     */
    private final class BeanReadPropertyImpl<P> implements UnsafeBeanReadProperty<B, P> {

        private final Argument<P> argument;
        private final AnnotationMetadata annotationMetadata;
        private final int getMethodIndex;

        private BeanReadPropertyImpl(BeanPropertyRef<P> ref) {
            this.argument = ref.readArgument;
            this.annotationMetadata = EvaluatedAnnotationMetadata.wrapIfNecessary(argument.getAnnotationMetadata());
            this.getMethodIndex = ref.getMethodIndex;
        }

        @NonNull
        @Override
        public String getName() {
            return argument.getName();
        }

        @NonNull
        @Override
        public Class<P> getType() {
            return argument.getType();
        }

        @Override
        @NonNull
        public Argument<P> asArgument() {
            return argument;
        }

        @NonNull
        @Override
        public BeanIntrospection<B> getDeclaringBean() {
            return AbstractInitializableBeanIntrospection.this;
        }

        @Override
        public AnnotationMetadata getAnnotationMetadata() {
            return annotationMetadata;
        }

        @Nullable
        @Override
        public P get(@NonNull B bean) {
            ArgumentUtils.requireNonNull("bean", bean);
            if (!beanType.isInstance(bean)) {
                throw new IllegalArgumentException("Invalid bean [" + bean + "] for type: " + beanType);
            }
            return dispatchOne(getMethodIndex, bean, null);
        }

        @Override
        public P getUnsafe(B bean) {
            return dispatchOne(getMethodIndex, bean, null);
        }

        @Override
        public String toString() {
            return "BeanReadProperty{" +
                    "beanType=" + beanType +
                    ", type=" + argument.getType() +
                    ", name='" + argument.getName() + '\'' +
                    '}';
        }
    }

    /**
     * Implementation of {@link BeanMethod} that is using {@link BeanMethodRef} and method dispatch.
     *
     * @param <P> The property type
     */
    private final class BeanMethodImpl<P> implements BeanMethod<B, P>, ExecutableMethod<B, P> {

        private final BeanMethodRef<P> ref;

        private BeanMethodImpl(BeanMethodRef<P> ref) {
            this.ref = ref;
        }

        @NonNull
        @Override
        public BeanIntrospection<B> getDeclaringBean() {
            return AbstractInitializableBeanIntrospection.this;
        }

        @Override
        public @NonNull
        ReturnType<P> getReturnType() {
            //noinspection unchecked
            return new ReturnType() {
                @Override
                public Class<P> getType() {
                    return ref.returnType.getType();
                }

                @Override
                @NonNull
                public Argument<P> asArgument() {
                    return ref.returnType;
                }

                @Override
                public Map<String, Argument<?>> getTypeVariables() {
                    return ref.returnType.getTypeVariables();
                }

                @NonNull
                @Override
                public AnnotationMetadata getAnnotationMetadata() {
                    return EvaluatedAnnotationMetadata.wrapIfNecessary(ref.returnType.getAnnotationMetadata());
                }
            };
        }

        @NonNull
        @Override
        public AnnotationMetadata getAnnotationMetadata() {
            return ref.annotationMetadata == null ? AnnotationMetadata.EMPTY_METADATA : ref.annotationMetadata;
        }

        @NonNull
        @Override
        public String getName() {
            return ref.name;
        }

        @Override
        public Argument<?>[] getArguments() {
            return ref.arguments == null ? Argument.ZERO_ARGUMENTS : ref.arguments;
        }

        @Override
        public P invoke(@NonNull B instance, Object... arguments) {
            return dispatch(ref.methodIndex, instance, arguments);
        }

        @Override
        public Method getTargetMethod() {
            if (ClassUtils.REFLECTION_LOGGER.isWarnEnabled()) {
                ClassUtils.REFLECTION_LOGGER.warn("Using getTargetMethod for method {} on type {} requires the use of reflection. GraalVM configuration necessary", getName(), getDeclaringType());
            }
            return getTargetMethodByIndex(ref.methodIndex);
        }

        @Override
        public Class<B> getDeclaringType() {
            return getDeclaringBean().getBeanType();
        }

        @Override
        public String getMethodName() {
            return getName();
        }

    }

    /**
     * Bean property compile-time data container.
     *
     * @param <P> The property type.
     */
    @Internal
    @UsedByGeneratedCode
    public static final class BeanPropertyRef<P> {
        @NonNull
        final Argument<P> argument;

        final int getMethodIndex;
        final int setMethodIndex;
        final int withMethodIndex;
        final boolean readyOnly;
        final boolean mutable;
        final boolean writeOnly;

        @Nullable
        final Argument<P> readArgument;
        @Nullable
        final Argument<P> writeArgument;

        public BeanPropertyRef(@NonNull Argument<P> argument,
                               int getMethodIndex,
                               int setMethodIndex,
                               int withMethodIndex,
                               boolean readyOnly,
                               boolean mutable) {
            this(argument, null, null, getMethodIndex, setMethodIndex, withMethodIndex, readyOnly, mutable);
        }

        public BeanPropertyRef(@NonNull Argument<P> argument,
                               @Nullable Argument<P> readArgument,
                               @Nullable Argument<P> writeArgument,
                               int getMethodIndex,
                               int setMethodIndex,
                               int withMethodIndex,
                               boolean readyOnly,
                               boolean mutable) {
            this.argument = argument;
            this.getMethodIndex = getMethodIndex;
            this.setMethodIndex = setMethodIndex;
            this.withMethodIndex = withMethodIndex;
            this.readyOnly = readyOnly;
            this.mutable = mutable;
            this.writeOnly = getMethodIndex == -1 && (setMethodIndex != -1 || withMethodIndex != -1);
            this.writeArgument = writeArgument == null && (setMethodIndex != -1 || withMethodIndex != -1) ? argument : writeArgument;
            this.readArgument = readArgument == null && (getMethodIndex != -1) ? argument : readArgument;
        }
    }

    /**
     * Bean method compile-time data container.
     *
     * @param <P> The property type.
     */
    @Internal
    @UsedByGeneratedCode
    public static final class BeanMethodRef<P> {
        @NonNull
        final Argument<P> returnType;
        @NonNull
        final String name;
        @Nullable
        final AnnotationMetadata annotationMetadata;
        @Nullable
        final Argument<?>[] arguments;

        final int methodIndex;

        public BeanMethodRef(@NonNull Argument<P> returnType,
                             @NonNull String name,
                             @Nullable AnnotationMetadata annotationMetadata,
                             @Nullable Argument<?>[] arguments,
                             int methodIndex) {
            this.returnType = returnType;
            this.name = name;
            this.annotationMetadata = EvaluatedAnnotationMetadata.wrapIfNecessary(annotationMetadata);
            this.arguments = arguments;
            this.methodIndex = methodIndex;
        }
    }

}
